{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ff6050dfa4dc1b6",
   "metadata": {},
   "source": [
    "## Lexicase Selection Numba加速\n",
    "\n",
    "DEAP中Lexicase Selection的默认实现速度较慢。因此，我们可以尝试使用Numba来加速它。\n",
    "Numba的原理是将Python代码编译为LLVM中间代码，然后再编译为机器码。从而显著提高Python代码的运行速度。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "59cfefc0467c74ad",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-12-25T03:39:37.866831400Z",
     "start_time": "2023-12-25T03:39:37.665471400Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import math\n",
    "import operator\n",
    "\n",
    "from deap import base, creator, tools, gp\n",
    "import time\n",
    "\n",
    "\n",
    "# 符号回归\n",
    "def evalSymbReg(individual, pset):\n",
    "    # 编译GP树为函数\n",
    "    func = gp.compile(expr=individual, pset=pset)\n",
    "    \n",
    "    # 使用numpy创建一个向量\n",
    "    x = np.linspace(-10, 10, 100) \n",
    "    \n",
    "    return tuple((func(x) - x**2)**2)\n",
    "\n",
    "\n",
    "# 创建个体和适应度函数，适应度数组大小与数据量相同\n",
    "creator.create(\"FitnessMin\", base.Fitness, weights=(-1.0,) * 100)  # 假设我们有100个数据点\n",
    "creator.create(\"Individual\", gp.PrimitiveTree, fitness=creator.FitnessMin)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "956e01e17271daa6",
   "metadata": {},
   "source": [
    "### 遗传算子\n",
    "在使用Numba进行对Lexicase加速时，只需要重写Lexicase函数，加上@njit(cache=True)这个注解就可以了。\n",
    "需要注意一些特殊的函数可能不受Numba支持，但所有基本的Python运算符都是支持的。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "c5dbeab9190022",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-12-25T03:39:37.891395500Z",
     "start_time": "2023-12-25T03:39:37.870837200Z"
    }
   },
   "outputs": [],
   "source": [
    "from numba import njit\n",
    "import numpy as np\n",
    "\n",
    "\n",
    "@njit(cache=True)\n",
    "def selAutomaticEpsilonLexicaseNumba(case_values, fit_weights, k):\n",
    "    selected_individuals = []\n",
    "    avg_cases = 0\n",
    "\n",
    "    for i in range(k):\n",
    "        candidates = list(range(len(case_values)))\n",
    "        cases = np.arange(len(case_values[0]))\n",
    "        np.random.shuffle(cases)\n",
    "\n",
    "        while len(cases) > 0 and len(candidates) > 1:\n",
    "            errors_for_this_case = np.array(\n",
    "                [case_values[x][cases[0]] for x in candidates]\n",
    "            )\n",
    "            median_val = np.median(errors_for_this_case)\n",
    "            median_absolute_deviation = np.median(\n",
    "                np.array([abs(x - median_val) for x in errors_for_this_case])\n",
    "            )\n",
    "            if fit_weights > 0:\n",
    "                best_val_for_case = np.max(errors_for_this_case)\n",
    "                min_val_to_survive = best_val_for_case - median_absolute_deviation\n",
    "                candidates = list(\n",
    "                    [\n",
    "                        x\n",
    "                        for x in candidates\n",
    "                        if case_values[x][cases[0]] >= min_val_to_survive\n",
    "                    ]\n",
    "                )\n",
    "            else:\n",
    "                best_val_for_case = np.min(errors_for_this_case)\n",
    "                max_val_to_survive = best_val_for_case + median_absolute_deviation\n",
    "                candidates = list(\n",
    "                    [\n",
    "                        x\n",
    "                        for x in candidates\n",
    "                        if case_values[x][cases[0]] <= max_val_to_survive\n",
    "                    ]\n",
    "                )\n",
    "            cases = np.delete(cases, 0)\n",
    "        avg_cases = (avg_cases * i + (len(case_values[0]) - len(cases))) / (i + 1)\n",
    "        selected_individuals.append(np.random.choice(np.array(candidates)))\n",
    "    return selected_individuals, avg_cases\n",
    "\n",
    "def selAutomaticEpsilonLexicaseFast(individuals, k):\n",
    "    fit_weights = individuals[0].fitness.weights[0]\n",
    "    case_values = np.array([ind.fitness.values for ind in individuals])\n",
    "    index, avg_cases = selAutomaticEpsilonLexicaseNumba(case_values, fit_weights, k)\n",
    "    selected_individuals = [individuals[i] for i in index]\n",
    "    return selected_individuals"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "783887f49d890b79",
   "metadata": {},
   "source": [
    "在定义好了新的Lexicase选择算子之后，在注册选择算子的时候，将新的选择算子注册进去就可以了。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "851794d4d36e3681",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-12-25T03:39:37.964779400Z",
     "start_time": "2023-12-25T03:39:37.897670100Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\zhenl\\anaconda3\\Lib\\site-packages\\deap\\gp.py:254: RuntimeWarning: Ephemeral rand101 function cannot be pickled because its generating function is a lambda function. Use functools.partial instead.\n",
      "  warnings.warn(\"Ephemeral {name} function cannot be \"\n"
     ]
    }
   ],
   "source": [
    "import random\n",
    "\n",
    "# 定义函数集合和终端集合\n",
    "pset = gp.PrimitiveSet(\"MAIN\", arity=1)\n",
    "pset.addPrimitive(operator.add, 2)\n",
    "pset.addPrimitive(operator.sub, 2)\n",
    "pset.addPrimitive(operator.mul, 2)\n",
    "pset.addPrimitive(operator.neg, 1)\n",
    "pset.addEphemeralConstant(\"rand101\", lambda: random.randint(-1, 1))\n",
    "pset.renameArguments(ARG0='x')\n",
    "\n",
    "# 定义遗传编程操作\n",
    "toolbox = base.Toolbox()\n",
    "toolbox.register(\"expr\", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)\n",
    "toolbox.register(\"individual\", tools.initIterate, creator.Individual, toolbox.expr)\n",
    "toolbox.register(\"population\", tools.initRepeat, list, toolbox.individual)\n",
    "toolbox.register(\"compile\", gp.compile, pset=pset)\n",
    "toolbox.register(\"evaluate\", evalSymbReg, pset=pset)\n",
    "toolbox.register(\"select\", selAutomaticEpsilonLexicaseFast)\n",
    "toolbox.register(\"mate\", gp.cxOnePoint)\n",
    "toolbox.register(\"mutate\", gp.mutUniform, expr=toolbox.expr, pset=pset)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62f30d17704db709",
   "metadata": {},
   "source": [
    "### 演化流程\n",
    "演化流程与传统符号回归相同。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "515b587d4f8876ea",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-12-25T03:39:39.571928900Z",
     "start_time": "2023-12-25T03:39:37.971234500Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   \t      \t                        fitness                        \t                      size                     \n",
      "   \t      \t-------------------------------------------------------\t-----------------------------------------------\n",
      "gen\tnevals\tavg    \tgen\tmax     \tmin\tnevals\tstd    \tavg\tgen\tmax\tmin\tnevals\tstd    \n",
      "0  \t100   \t5275.44\t0  \t1.21e+06\t0  \t100   \t42915.8\t4  \t0  \t7  \t2  \t100   \t1.54919\n",
      "1  \t91    \t5008.21\t1  \t1.21e+06\t0  \t91    \t52501.4\t4.71\t1  \t14 \t3  \t91    \t2.15543\n",
      "2  \t92    \t8333.56\t2  \t1.21e+06\t0  \t92    \t67566.8\t4.61\t2  \t9  \t3  \t92    \t1.88624\n",
      "3  \t89    \t5219.06\t3  \t1.21e+06\t0  \t89    \t52574.6\t4.44\t3  \t13 \t2  \t89    \t1.9252 \n",
      "4  \t89    \t5054.56\t4  \t1.21e+06\t0  \t89    \t52499.8\t4.45\t4  \t11 \t3  \t89    \t2.02176\n",
      "5  \t92    \t8001.89\t5  \t1.21e+06\t0  \t92    \t67135.2\t4.58\t5  \t11 \t3  \t92    \t2.04539\n",
      "6  \t85    \t9893.52\t6  \t1.21e+06\t0  \t85    \t73912.4\t4.71\t6  \t10 \t3  \t85    \t1.89365\n",
      "7  \t93    \t5239.52\t7  \t1.21e+06\t0  \t93    \t52304.6\t4.62\t7  \t11 \t3  \t93    \t1.9989 \n",
      "8  \t94    \t130452 \t8  \t1.0201e+08\t0  \t94    \t2.67865e+06\t4.88\t8  \t13 \t3  \t94    \t2.21486\n",
      "9  \t84    \t6943.47\t9  \t1.21e+06  \t0  \t84    \t60594      \t4.96\t9  \t17 \t2  \t84    \t2.72734\n",
      "10 \t93    \t477383 \t10 \t3.9601e+08\t0  \t93    \t1.03626e+07\t4.75\t10 \t13 \t3  \t93    \t2.74727\n",
      "mul(x, x)\n",
      "   \t      \t                    fitness                    \t                      size                     \n",
      "   \t      \t-----------------------------------------------\t-----------------------------------------------\n",
      "gen\tnevals\tavg    \tgen\tmax  \tmin\tnevals\tstd    \tavg \tgen\tmax\tmin\tnevals\tstd    \n",
      "0  \t100   \t1975.21\t0  \t40000\t0  \t100   \t3054.43\t4.24\t0  \t7  \t2  \t100   \t1.83368\n",
      "1  \t91    \t6506.41\t1  \t1.21e+06\t0  \t91    \t60537.9\t4.33\t1  \t9  \t2  \t91    \t1.61898\n",
      "2  \t90    \t11200.3\t2  \t1.21e+06\t0  \t90    \t79757.9\t4.26\t2  \t11 \t3  \t90    \t1.63475\n",
      "3  \t90    \t4948.46\t3  \t1.21e+06\t0  \t90    \t52503.9\t4.03\t3  \t9  \t3  \t90    \t1.4863 \n",
      "4  \t87    \t11303.4\t4  \t1.21e+06\t0  \t87    \t79801.3\t3.96\t4  \t9  \t2  \t87    \t1.50944\n",
      "5  \t86    \t14149.8\t5  \t4.41e+06\t0  \t86    \t136105 \t3.99\t5  \t9  \t3  \t86    \t1.49328\n",
      "6  \t86    \t11076.8\t6  \t1.21e+06\t0  \t86    \t79383.5\t3.91\t6  \t10 \t3  \t86    \t1.56904\n",
      "7  \t92    \t9538.33\t7  \t1.21e+06\t0  \t92    \t73545.9\t3.69\t7  \t9  \t3  \t92    \t1.33937\n",
      "8  \t94    \t4824.25\t8  \t1.21e+06\t0  \t94    \t51932.3\t3.57\t8  \t9  \t3  \t94    \t1.39467\n",
      "9  \t91    \t4883.65\t9  \t1.21e+06\t0  \t91    \t52520.1\t3.37\t9  \t9  \t3  \t91    \t0.986458\n",
      "10 \t86    \t3370.4 \t10 \t1.21e+06\t0  \t86    \t42938.1\t3.47\t10 \t9  \t3  \t86    \t1.25264 \n",
      "mul(x, x)\n",
      "   \t      \t                    fitness                    \t                      size                     \n",
      "   \t      \t-----------------------------------------------\t-----------------------------------------------\n",
      "gen\tnevals\tavg    \tgen\tmax  \tmin\tnevals\tstd    \tavg\tgen\tmax\tmin\tnevals\tstd    \n",
      "0  \t100   \t2225.68\t0  \t44100\t0  \t100   \t3764.38\t3.8\t0  \t7  \t2  \t100   \t1.66132\n",
      "1  \t85    \t3137.36\t1  \t1.21e+06\t0  \t85    \t42922.2\t3.15\t1  \t9  \t3  \t85    \t0.71239\n",
      "2  \t95    \t1851.37\t2  \t1.21e+06\t0  \t95    \t30475.6\t3.21\t2  \t9  \t3  \t95    \t0.778396\n",
      "3  \t88    \t1560.67\t3  \t980100  \t0  \t88    \t28709.3\t3.17\t3  \t9  \t2  \t88    \t0.860872\n",
      "4  \t82    \t84.6141\t4  \t12100   \t0  \t82    \t701.567\t3.19\t4  \t7  \t2  \t82    \t0.783518\n",
      "5  \t90    \t5009.87\t5  \t1.21e+06\t0  \t90    \t52529.5\t3.47\t5  \t9  \t3  \t90    \t1.2685  \n",
      "6  \t91    \t1954.46\t6  \t1.21e+06\t0  \t91    \t30572  \t3.56\t6  \t11 \t3  \t91    \t1.58947 \n",
      "7  \t93    \t119026 \t7  \t1.1881e+08\t0  \t93    \t2.64798e+06\t3.23\t7  \t9  \t2  \t93    \t1.01838 \n",
      "8  \t79    \t3325.04\t8  \t1.21e+06  \t0  \t79    \t42938.1    \t3.19\t8  \t7  \t2  \t79    \t0.716868\n",
      "9  \t92    \t1934.68\t9  \t1.21e+06  \t0  \t92    \t30571.3    \t3.4 \t9  \t9  \t3  \t92    \t1.13137 \n",
      "10 \t92    \t397.156\t10 \t40000     \t0  \t92    \t2548.25    \t3.32\t10 \t9  \t2  \t92    \t1.00876 \n",
      "mul(x, x)\n"
     ]
    }
   ],
   "source": [
    "import numpy\n",
    "from deap import algorithms\n",
    "\n",
    "# 定义统计指标\n",
    "stats_fit = tools.Statistics(lambda ind: ind.fitness.values)\n",
    "stats_size = tools.Statistics(len)\n",
    "mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size)\n",
    "mstats.register(\"avg\", numpy.mean)\n",
    "mstats.register(\"std\", numpy.std)\n",
    "mstats.register(\"min\", numpy.min)\n",
    "mstats.register(\"max\", numpy.max)\n",
    "\n",
    "# 使用Numba加速\n",
    "numba_lexicase_time = []\n",
    "for i in range(3):\n",
    "    start = time.time()\n",
    "    population = toolbox.population(n=100)\n",
    "    hof = tools.HallOfFame(1)\n",
    "    pop, log  = algorithms.eaSimple(population=population,\n",
    "                               toolbox=toolbox, cxpb=0.9, mutpb=0.1, ngen=10, stats=mstats, halloffame=hof, verbose=True)\n",
    "    end = time.time()\n",
    "    print(str(hof[0]))\n",
    "    numba_lexicase_time.append(end - start)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "98efab3037e032ea",
   "metadata": {},
   "source": [
    "为了展示Numba加速的效果，我们将使用纯Python实现的Lexicase Selection进行对比。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "e1efd33d5f96a536",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-12-25T03:45:33.479324Z",
     "start_time": "2023-12-25T03:39:39.565415500Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   \t      \t                        fitness                        \t                      size                     \n",
      "   \t      \t-------------------------------------------------------\t-----------------------------------------------\n",
      "gen\tnevals\tavg    \tgen\tmax     \tmin\tnevals\tstd    \tavg \tgen\tmax\tmin\tnevals\tstd    \n",
      "0  \t100   \t3651.98\t0  \t1.21e+06\t0  \t100   \t30493.7\t4.67\t0  \t7  \t2  \t100   \t1.74387\n",
      "1  \t90    \t441.554\t1  \t44100   \t0  \t90    \t2602.04\t3.36\t1  \t9  \t3  \t90    \t1.16207\n",
      "2  \t91    \t293.089\t2  \t44100   \t0  \t91    \t2139.02\t3.25\t2  \t8  \t3  \t91    \t0.876071\n",
      "3  \t90    \t1726.19\t3  \t1.21e+06\t0  \t90    \t30422  \t3.28\t3  \t9  \t3  \t90    \t1.0008  \n",
      "4  \t95    \t4864.87\t4  \t1.21e+06\t0  \t95    \t52507.2\t3.43\t4  \t9  \t3  \t95    \t1.21041 \n",
      "5  \t91    \t145.704\t5  \t10000   \t0  \t91    \t902.638\t3.35\t5  \t9  \t3  \t91    \t1.21963 \n",
      "6  \t84    \t278.787\t6  \t40000   \t0  \t84    \t1736.06\t3.3 \t6  \t9  \t2  \t84    \t1.09087 \n",
      "7  \t96    \t1600.26\t7  \t1.21e+06\t0  \t96    \t30392.7\t3.08\t7  \t7  \t3  \t96    \t0.483322\n",
      "8  \t84    \t167.528\t8  \t40000   \t0  \t84    \t1547.77\t3.22\t8  \t8  \t3  \t84    \t0.819512\n",
      "9  \t98    \t1895.42\t9  \t1.2321e+06\t0  \t98    \t31134.9\t3.22\t9  \t9  \t2  \t98    \t0.878408\n",
      "10 \t87    \t4718.15\t10 \t1.21e+06  \t0  \t87    \t52498.5\t3.27\t10 \t8  \t3  \t87    \t0.903936\n",
      "mul(x, x)\n",
      "   \t      \t                        fitness                        \t                      size                     \n",
      "   \t      \t-------------------------------------------------------\t-----------------------------------------------\n",
      "gen\tnevals\tavg    \tgen\tmax     \tmin\tnevals\tstd    \tavg \tgen\tmax\tmin\tnevals\tstd    \n",
      "0  \t100   \t3562.89\t0  \t1.44e+06\t0  \t100   \t33301.1\t4.08\t0  \t7  \t2  \t100   \t1.65336\n",
      "1  \t99    \t129543 \t1  \t1.0201e+08\t0  \t99    \t2.67852e+06\t4.81\t1  \t11 \t3  \t99    \t1.78154\n",
      "2  \t87    \t10353.5\t2  \t4.41e+06  \t0  \t87    \t125928     \t4.23\t2  \t9  \t2  \t87    \t1.56751\n",
      "3  \t89    \t8502.77\t3  \t1.21e+06  \t0  \t89    \t66867.3    \t4.08\t3  \t11 \t3  \t89    \t1.54065\n",
      "4  \t95    \t122728 \t4  \t1.1881e+08\t0  \t95    \t2.64816e+06\t3.87\t4  \t11 \t3  \t95    \t1.5143 \n",
      "5  \t91    \t7960.63\t5  \t4.41e+06  \t0  \t91    \t122289     \t3.59\t5  \t11 \t2  \t91    \t1.47713\n",
      "6  \t91    \t1998.09\t6  \t1.21e+06  \t0  \t91    \t30454.9    \t3.42\t6  \t7  \t2  \t91    \t0.929301\n",
      "7  \t87    \t3408.27\t7  \t1.21e+06  \t0  \t87    \t42495.1    \t3.33\t7  \t9  \t3  \t87    \t0.990505\n",
      "8  \t89    \t1975.59\t8  \t1e+06     \t0  \t89    \t29499.2    \t3.45\t8  \t8  \t2  \t89    \t1.26787 \n",
      "9  \t88    \t1808.82\t9  \t1.44e+06  \t0  \t88    \t33282.9    \t3.39\t9  \t9  \t2  \t88    \t1.24816 \n",
      "10 \t90    \t251.822\t10 \t40000     \t0  \t90    \t1693.86    \t3.22\t10 \t7  \t3  \t90    \t0.715262\n",
      "sub(mul(x, x), 0)\n",
      "   \t      \t                    fitness                    \t                      size                     \n",
      "   \t      \t-----------------------------------------------\t-----------------------------------------------\n",
      "gen\tnevals\tavg    \tgen\tmax  \tmin\tnevals\tstd    \tavg \tgen\tmax\tmin\tnevals\tstd    \n",
      "0  \t100   \t2222.76\t0  \t44100\t0  \t100   \t3940.18\t3.86\t0  \t7  \t2  \t100   \t1.60012\n",
      "1  \t94    \t4969.96\t1  \t1.21e+06\t0  \t94    \t52503.5\t4.03\t1  \t7  \t3  \t94    \t1.56496\n",
      "2  \t93    \t9537.67\t2  \t1.21e+06\t0  \t93    \t73934.3\t4.15\t2  \t10 \t3  \t93    \t1.77975\n",
      "3  \t94    \t2020.59\t3  \t1.21e+06\t0  \t94    \t30526.7\t3.92\t3  \t9  \t3  \t94    \t1.69517\n",
      "4  \t96    \t209.845\t4  \t40000   \t0  \t96    \t1618.7 \t3.78\t4  \t11 \t2  \t96    \t1.61604\n",
      "5  \t94    \t8063.67\t5  \t1.21e+06\t0  \t94    \t67163.1\t4.1 \t5  \t9  \t2  \t94    \t1.81934\n",
      "6  \t86    \t5093.5 \t6  \t1.21e+06\t0  \t86    \t52525.1\t4.19\t6  \t12 \t3  \t86    \t2.09139\n",
      "7  \t88    \t3409.3 \t7  \t1.21e+06\t0  \t88    \t42937.1\t3.88\t7  \t11 \t3  \t88    \t1.94566\n",
      "8  \t95    \t459.597\t8  \t90000   \t0  \t95    \t3741.5 \t3.96\t8  \t11 \t3  \t95    \t2.08768\n",
      "9  \t88    \t9639.39\t9  \t4.41e+06\t0  \t88    \t125915 \t4.04\t9  \t13 \t3  \t88    \t2.10675\n",
      "10 \t88    \t3473.43\t10 \t1.21e+06\t0  \t88    \t42953.7\t3.5 \t10 \t9  \t3  \t88    \t1.253  \n",
      "mul(x, x)\n"
     ]
    }
   ],
   "source": [
    "# 使用纯Python实现的Lexicase Selection\n",
    "toolbox.register(\"select\", tools.selAutomaticEpsilonLexicase)\n",
    "python_lexicase_time = []\n",
    "for i in range(3):\n",
    "    start = time.time()\n",
    "    population = toolbox.population(n=100)\n",
    "    hof = tools.HallOfFame(1)\n",
    "    pop, log  = algorithms.eaSimple(population=population,\n",
    "                               toolbox=toolbox, cxpb=0.9, mutpb=0.1, ngen=10, stats=mstats, halloffame=hof, verbose=True)\n",
    "    end = time.time()\n",
    "    print(str(hof[0]))\n",
    "    python_lexicase_time.append(end - start)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adae32e725d21dcf",
   "metadata": {},
   "source": [
    "下面是Numba加速和纯Python实现的Lexicase Selection的运行时间对比。从结果可以看出，Numba加速后的Lexicase Selection的运行速度远优于纯Python实现的Lexicase Selection。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "3caf7686fef519a",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-12-25T03:45:33.641210400Z",
     "start_time": "2023-12-25T03:45:33.479324Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 400x300 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import seaborn as sns\n",
    "import matplotlib.pyplot as plt\n",
    "import pandas as pd\n",
    "\n",
    "data = pd.DataFrame(\n",
    "    {'Category': ['Numba Lexicase'] * len(numba_lexicase_time) + ['Python Lexicase'] * len(python_lexicase_time),\n",
    "     'Time': np.concatenate([numba_lexicase_time, python_lexicase_time])})\n",
    "\n",
    "plt.figure(figsize=(4, 3))\n",
    "sns.set_style(\"whitegrid\")\n",
    "sns.boxplot(data=data, x='Category', y='Time', palette=\"Set3\", width=0.4)\n",
    "plt.title('Comparison of Numba and Pure Python')\n",
    "plt.xlabel('')\n",
    "plt.ylabel('Time')\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
